<?php
/**
 * @file 3gplex/includes/dbitem.inc
 * 
 * 
 */

/**
 * Database stored item (and corresponding table).
 * 
 * 
 * @note you can get object composition using a join statement as table,
 * however this never has been tested.
 */
abstract class plex_dbitem
{
	private $primary_key = null;
	private $table;
	private $fields = array();
	/**
	 * Table columns (has to be overriden).
	 * Name indexed array array('colname'=>'type(size) maybenot null ...').
	 */
	private static $columns = array();

	/**
	 * Returns columns (has to be overriden).
	 * There is no way to reference a static member using a variable and no late static biding for now.
	 */
	protected function get_columns()
	{
		return self::$columns;
	}

	/**
	 * Return a new object the kind of your class (has to be overriden).
	 * 
	 * You know the deal, no way to reference a static member using a
	 * variable and no late static binding PHP 5.3.x bla bla... You have
	 * to implement it also.
	 * 
	 * @note Overriding this function you can implement the factory and
	 * singleton patterns - look for hints in the plex_module override
	 * of this function along with the plex_modules class.
	 */
	protected function myself($row)
	{
		return new self;
	}

	static function table_exists($table)
	{
		return db_table_exists($table);
	}

	static function show_table($table)
	{
		if (!db_table_exists($table)) return false;
		$res = db_query("show create table {${table}}");
		if ($res !== false) {
			$row = db_fetch_array($res);
			return $row['Create Table'];
		}
		return false;
	}

	function show()
	{
		return self::show_table($this->table);
	}

	function create()
	{
		$table = $this->table;
		if (db_table_exists($table)) return true;
		$prefields = array_keys($this->fields);
		$postfields = array_keys($this->get_columns());
		foreach ($prefields as $prefield) {
			if (!in_array($prefield, $postfields)) {
				echo "<h2>prefield not in postfields</h2>";
				return false;
			}
		}
		foreach ($postfields as $postfield) {
			if (!in_array($postfield, $prefields)) {
				echo "<h2>postfield {$postfield} not in prefields(".join(', ', $prefields).")</h2>";
				return false;
			}
		}
		$columns = "";
		foreach ($types as $column=>$type) {
			if ($columns === "") {
				$columns = "`${column}` ${type}";
			} else {
				$columns .= ", `${column}` ${type}";
			}
		}
		return db_query("create table {${table}} (${columns})");
	}

	static function drop_table($table)
	{
		return db_query("drop table {${table}}");
	}

	function drop()
	{
		return self::drop_table($this->table);
	}

	/**
	 * Gets actual table definition from database.
	 * 
	 * @note Fields for now are expressed as a string. This forbids
	 * to implement type change for columns.
	 * 
	 */
	function get_schema()
	{
		if (!db_table_exists($this->table)) return array();
		$res = db_query("describe {{$this->table}}");
		if ($res === false) return false;
		$cols = array();
		while ($col = db_fetch_object($res)) {
			$cols[$col->Field] = $col->Type;
			if ($col->Null === "NO") $cols[$col->Field] .= " not null";
			if (strlen($col->Extra) > 0) $cols[$col->Field] .= " $col->Extra";
			if ($col->Key === "PRI") $cols[$col->Field] .= "primary key";
			if ($col->Key === "UNI") $cols[$col->Field] .= "unique key";
		}
		return $cols;
	}

	/**
	 * Updates table schema.
	 * 
	 * 
	 */
	function update_schema()
	{
		$before = $this->get_schema();
		$beforekeys = array_keys($before);
		$after = $this->get_columns();
		$afterkeys = array_keys($after);

		$rem = array();
		foreach ($beforekeys as $key) {
			if (!in_array($key, $afterkeys)) $rem[$key] = $before[$key];
		}
		$add = array();
		foreach ($afterkeys as $key) {
			if (!in_array($key, $beforekeys)) $add[$key] = $after[$key];
		}
		if (!db_table_exists($this->table)) {
			$columns = "";
			foreach ($after as $column=>$type) {
				if ($columns === "") {
					$columns = "`${column}` ${type}";
				} else {
					$columns .= ", `${column}` ${type}";
				}
			}
			plex::notice("Create table $table");
			return db_query("create table {{$this->table}} (${columns})");
		}
		if (count($rem) > 0) {
			$columns = "";
			foreach ($rem as $column=>$type) {
				if ($columns === "")	$columns = "`{$column}`";
				else			$columns .= ", drop `{$column}`";
				if (method_exists($this, "schema_drop_{$column}")) call_user_func(array($this, "schema_drop_{$column}"));
			}
			plex::notice("Alter table {$table} remove {$columns}");
			return db_query("alter table {{$this->table}} drop {$columns}");
		}
		if (count($add) > 0) {
			$columns = "";
			foreach ($add as $column=>$type) {
				if ($columns === "")	$columns = "`${column}` {$type}";
				else			$columns .= ", `{$column}` {$type}";
				if (method_exists($this, "schema_add_{$column}")) call_user_func(array($this, "schema_add_{$column}"));
			}
			plex::notice("Alter table $this->table add ${columns}");
			return db_query("alter table {{$this->table}} add column ({$columns})");
		}
		return true;
	}

	function __construct($table, $primary_key_field, array $fields)
	{
		$this->table = $table;
		$this->primary_key = $primary_key_field;
		foreach ($fields as $key) {
			$this->fields[$key] = null;
		}
		$this->fields[$primary_key_field] = null;
	}

	/**
	 *
	 * @note You can't set primary key using this method.
	 */
	function __set($field, $value)
	{
		if (array_key_exists($field, $this->fields) && $field !== $primary_key) {
			$this->fields[$field] = $value;
			return true;
		}
		return false;
	}

	/**
	 * 
	 */
	function __get($field)
	{
		return $this->fields[$field];
	}

	/**
	 * Override this method to get a different result when converting your class
	 * to string.
	 * Default inherited one is to consider the primary field as the projection from object
	 * space to string space.
	 */
	function __toString()
	{
		return (string)$this->fields[$this->primary_key];
	}

	/**
	 * Loads fields from a database record which complies to a certain pattern
	 * (default: equality of primary key).
	 * 
	 * Specify the pattern as a printf-style string in $condition like e.g.
	 * <tt>field_name='%s' and other_field='%d'</tt>. The printf style
	 * escape patterns will be replaced by their corresponding values in $is.
	 *
	 * Some call samples:
	 * @li <tt>$item->load(3, "buyers=%d")</tt>
	 * @li <tt>$item->load(array(200, 10), "lovers > %d and haters < %d")</tt>
	 * @li <tt>$item->load("John Doe", "fullname='%s'")
	 *
	 * $condition is optional and defaults to a pattern which tests equality
	 * of $is with primary key field.
	 * 
	 * @note This is not a collection retrieval method, for this look to select().
	 * @note This method is not static and operate on one already created object and
	 * won't convert it to the most specialised object class, again, look to select()
	 * and myself().
	 */
	function load($is, $condition = null)
	{
		$table = $this->table;
		$primary_key = $this->primary_key;
		if ($condition === null) { $condition = "${primary_key}='%s'"; }
		$res = db_query("select * from {${table}} where ${condition}", $is);
		if ($res === false) {
			return false;
		}
		if (db_num_rows($res) !== 1) {
			return false;
		}
		$row = db_fetch_array($res);
		$this->set_from_row($row);
		return $res;
	}

	/**
	 * Sets fields according to their value in database.
	 * 
	 * All fields in array $values will see their value overwriting the ones of the
	 * object. However you can set your own conversion method for each field.
	 * Define for this in your class inheriting from dbitem a method with this signature:
	 * <tt>mixed read_{field}(mixed $value)</tt> where @c {field} is the name of the
	 * field you would like to see converted prior to overwriting object one.
	 * 
	 * @note It is dangerous to set a 'read' conversion hook for the primary key.
	 * @note There is no type checking, this is open to discussion.
	 */
	protected function set_from_row(array $values)
	{
		foreach ($values as $key=>$value) {
			$hook = "read_$key";
			if (method_exists($this, $hook))
				$this->fields[$key] = $this->$hook($value);
			else	$this->fields[$key] = $value;
		}
	}

	/**
	 * Insert dbitem to table.
	 *
	 * All fields defined as columns will see their value inserted to the row. However
	 * you can set your own conversion method for each field. Define for this in your
	 * class inheriting from dbitem a method with this signature:
	 * <tt>mixed write_{field}(mixed $value)</tt> where @c {field} is the name of the
	 * field you would like to see converted prior to write.
	 * 
	 * @note It is useless to set a 'write' conversion hook for the primary key.
	 */
	function insert($moron = false)
	{
		$table = $this->table;
		$values = array();
		foreach ($this->fields as $key=>$value) {
			if ($key === $this->primary_key) continue;
			if ($value === null) {
				if ($moron) {
					plex::warn("Can't add row to table {$table} as column {$key} lacks value");
					return false;
				} else continue;
			}
			if (count($values) === 0) {
				$names = "${key}";
				$placeholders = "'%s'";
			} else {
				$names .= ", ${key}";
				$placeholders .= ", '%s'";
			}
			$hook = "write_$key";
			if (method_exists($this, $hook))
				$values[] = $this->$hook($value);
			else	$values[] = $value;
		}
		$res = db_query("insert into {${table}} (${names}) values (${placeholders})", $values);
		if ($res !== false) $this->fields[$primary_key] = mysql_insert_id();
		return $res;
	}

	static function update_row($table, $primary_key) { ; }

	/**
	 * Updates dbitem in table.
	 *
	 * All fields defined as columns will see their value updated to the row (whether
	 * they changed or not). As with insert() you can set your own conversion method
	 * for each field. Define for this in your
	 * class inheriting from dbitem a method with this signature:
	 * <tt>mixed write_{field}(mixed $value)</tt> where @c {field} is the name of the
	 * field you would like to see converted prior to write.
	 * 
	 * @note It is useless to set a 'write' conversion hook for the primary key.
	 * @note The $is value(s) won't be automatically converted, this is open to discussion.
	 */
	function update($is = null, $condition = null)
	{
		$table = $this->table;
		$primary_key = $this->primary_key;
		if ($condition === null) { $condition = "${primary_key}='%s'"; }
		if ($is === null) { $is = $this->fields[$primary_key]; }
		$values = array();
		foreach ($this->fields as $key=>$value) {
			if ($value === null) continue;
			if ($key === $primary_key) continue;
			if (count($values)===0) {
				$settings = "set ${key}='%s'";
			} else {
				$settings .= ", ${key}='%s'";
			}
			$hook = "write_$key";
			if (method_exists($this, $hook)) $values[] = $this->$hook($value);
			else $values[] = $value;
		}
		foreach ((array)$is as $single) {
			$values[] = $single;
		}
		$query = "update {${table}} ${settings} where ${condition}";
		plex::warn($query." => [ mid:{$this->fields['mid']} module:{$this->fields['module']} plexed:{$this->fields['plexed']} ]");
		try {
			if ($this->fields['mid'] == 3 && $this->fields['plexed'] == 0)
				throw new plex_debug_exception;
		} catch (plex_debug_exception $err) {
			$err->display();
		}
		return db_query("update {${table}} ${settings} where ${condition}", $values);
	}

	static function delete_rows($table, $is, $condition)
	{
		return db_query("delete from {${table}} where ${condition}", (array)$is);
	}
	
	function delete($is = null, $condition = null)
	{
		$table = $this->table;
		$primary_key = $this->primary_key;
		if ($condition === null) { $condition = "${primary_key}='%s'"; }
		if ($is === null) { $is = $this->fields[$primary_key]; }
		return db_query("delete from {${table}} where ${condition}", (array)$is);
	}

	
	/**
	 * 
	 */
	function select($is = null, $condition = null, $from = false, $count = false)
	{
		$table = $this->table;
		$primary_key = $this->primary_key;

		$query = "select * from {${table}}";
		if ($condition !== null) { $query .= " where ${condition}"; }
		if ($is === null) { $is = array(); }
		if ($from === false || $count === false) {
			$res = db_query($query, (array)$is);
		} else {
			$res = db_query_range($query, (array)$is, $from, $count);
		}
		$results = array();
		while ($row = db_fetch_array($res)) {
			$new = $this->myself($row);
			$new->set_from_row($row);
			$results[] = $new;
		}
		return $results;
	}
}

